// SPDX-License-Identifier: GPL-2.0-only
/*
 * Based on kernel/power/suspend.c
 *
 * Copyright (c) 2003 Patrick Mochel
 * Copyright (c) 2003 Open Source Development Lab
 * Copyright (c) 2009 Rafael J. Wysocki <rjw@sisk.pl>, Novell Inc.
 * Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
 * Author: Huawei OS Kernel Lab
 * Create: Thu Aug 15 16:14:23 2023
 */

#include <linux/cpu.h>
#include <linux/export.h>
#include <linux/suspend.h>
#include <linux/syscore_ops.h>
#include <linux/stop_machine.h>
#include <liblinux/pal.h>

#include "suspend_fail_notifier.h"

#ifdef CONFIG_SUSPEND
suspend_state_t pm_suspend_target_state;
EXPORT_SYMBOL_GPL(pm_suspend_target_state);
#endif

#ifdef CONFIG_LIBLINUX_HAVE_SUSPEND
static const struct platform_suspend_ops dummy_ops = {
	.begin = NULL,
	.prepare = NULL,
	.prepare_late = NULL,
	.wake = NULL,
	.finish = NULL,
	.end = NULL,
	.recover = NULL,
};
static const struct platform_suspend_ops *suspend_ops = &dummy_ops;
static const struct platform_s2idle_ops *s2idle_ops;

void suspend_set_ops(const struct platform_suspend_ops *ops)
{
	suspend_ops = ops;
}

enum {
	DEV_RESUME_NOIRQ,
	DEV_RESUME_EARLY,
	DEV_RESUME_END,
	PLAT_RESUME_NOIRQ,
	PLAT_RESUME_FINISH,
	PLAT_RESUME_END,
	PLAT_RECOVER,
	PLAT_SUSPEND_PREPARE,
	PLAT_SUSPEND_NOIRQ,
	PLAT_SUSPEND_BEGIN,
};

/* default implementation */
void __weak arch_suspend_disable_irqs(void)
{
	local_irq_disable();
}

/* default implementation */
void __weak arch_suspend_enable_irqs(void)
{
	local_irq_enable();
}

static int platform_suspend(suspend_state_t state, unsigned int step)
{
	int ret = 0;

	if (state != PM_SUSPEND_TO_IDLE) {
		switch (step) {
		case (unsigned int)PLAT_SUSPEND_PREPARE:
			ret = suspend_ops->prepare ? suspend_ops->prepare() : 0;
			break;
		case (unsigned int)PLAT_SUSPEND_NOIRQ:
			ret = suspend_ops->prepare_late ? suspend_ops->prepare_late() : 0;
			break;
		case (unsigned int)PLAT_SUSPEND_BEGIN:
			ret = suspend_ops->begin ? suspend_ops->begin(state) : 0;
			break;
		}
	} else {
		if (step == (unsigned int)PLAT_SUSPEND_BEGIN)
			if (s2idle_ops && s2idle_ops->begin)
				return s2idle_ops->begin();
	}
	return ret;
}

static void platform_resume(suspend_state_t state, unsigned int step)
{
	if (state != PM_SUSPEND_TO_IDLE) {
		switch (step) {
		case (unsigned int)PLAT_RESUME_NOIRQ:
			if (suspend_ops->wake != NULL)
				suspend_ops->wake();
			break;
		case (unsigned int)PLAT_RESUME_FINISH:
			if (suspend_ops->finish != NULL)
				suspend_ops->finish();
			break;
		case (unsigned int)PLAT_RESUME_END:
			if (suspend_ops->end != NULL)
				suspend_ops->end();
			break;
		}
	} else {
		if (step == (unsigned int)PLAT_RESUME_END)
			if (s2idle_ops && s2idle_ops->end)
				return s2idle_ops->end();
	}
}

static void platform_recover(suspend_state_t state)
{
	if (state != PM_SUSPEND_TO_IDLE && suspend_ops->recover)
		suspend_ops->recover();
}

static int enable_nonboot_cpu_threads(void *_param)
{
	unsigned int cpu = smp_processor_id();

	liblinux_pal_process_set_slv(cpu, 0);
	return 0;
}

static int freeze_nonboot_cpu_threads(void *_param)
{
	unsigned int cpu = smp_processor_id();

	liblinux_pal_process_set_slv(cpu, PROCESS_SCHED_LEVEL_MAX);
	return 0;
}

static int nonboot_cpu_threads_handle(cpu_stop_fn_t fn)
{
	int error = 0;
	struct cpumask nonboot_cpumask;

	cpumask_copy(&nonboot_cpumask, cpu_online_mask);
	cpumask_clear_cpu(0, &nonboot_cpumask);
	error = stop_machine_cpuslocked(fn, NULL, &nonboot_cpumask);
	if (error != 0)
		pr_err("failed to handle nonboot cpu's threads, error=%d\n", error);

	return error;
}

static void liblinux_suspend_prepare(void)
{
	cpu_hotplug_disable();
}

static void liblinux_resume_finish(void)
{
	cpu_hotplug_enable();
}

static void liblinux_resume_rollback(unsigned int step)
{
	suspend_state_t state = PM_SUSPEND_MEM;

	pr_info("liblinux resume rollback start, step: %u\n", step);
	switch (step) {
	case (unsigned int)PLAT_RESUME_NOIRQ:
		platform_resume(state, PLAT_RESUME_NOIRQ);
		break;
	case (unsigned int)DEV_RESUME_NOIRQ:
		dpm_resume_noirq(PMSG_RESUME);
		fallthrough;
	case (unsigned int)DEV_RESUME_EARLY:
		dpm_resume_early(PMSG_RESUME);
		fallthrough;
	case (unsigned int)PLAT_RESUME_FINISH:
		platform_resume(state, PLAT_RESUME_FINISH);
		fallthrough;
	case (unsigned int)DEV_RESUME_END:
		dpm_resume_end(PMSG_RESUME);
		fallthrough;
	case (unsigned int)PLAT_RESUME_END:
		platform_resume(state, PLAT_RESUME_END);
		liblinux_resume_finish();
		break;
	default:
		break;
	}
	pr_info("liblinux resume rollback end\n");
}

static void liblinux_platform_recover(void)
{
	platform_recover(PM_SUSPEND_MEM);
	liblinux_resume_rollback(DEV_RESUME_END);
}
#endif

void liblinux_resume(void)
{
#ifdef CONFIG_LIBLINUX_HAVE_SUSPEND
	(void)nonboot_cpu_threads_handle(enable_nonboot_cpu_threads);
	liblinux_resume_rollback(DEV_RESUME_NOIRQ);
#endif
}
EXPORT_SYMBOL(liblinux_resume);

int liblinux_suspend(void)
{
#ifdef CONFIG_LIBLINUX_HAVE_SUSPEND
	int error;
	suspend_state_t state = PM_SUSPEND_MEM;

	pr_info("liblinux suspend start\n");
	liblinux_suspend_prepare();

	if (!pm_suspend_should_fail(PM_TEST_PLAT_BEGIN, &error))
		error = platform_suspend(state, PLAT_SUSPEND_BEGIN);
	if (error != 0) {
		pr_err("platform failed to begin suspend, error=%d\n", error);
		liblinux_resume_rollback(PLAT_RESUME_END);
		goto exit;
	}

	if (!pm_suspend_should_fail(PM_TEST_DPM_START, &error))
		error = dpm_suspend_start(PMSG_SUSPEND);
	if (error != 0) {
		pr_err("Some devices failed to suspend, or early wake event detected, error=%d\n",
				error);
		liblinux_platform_recover();
		goto exit;
	}

	if (!pm_suspend_should_fail(PM_TEST_PLAT_PRE, &error))
		error = platform_suspend(state, PLAT_SUSPEND_PREPARE);
	if (error != 0) {
		pr_err("platform failed to prepare suspend, error=%d\n", error);
		liblinux_resume_rollback(PLAT_RESUME_FINISH);
		goto exit;
	}

	if (!pm_suspend_should_fail(PM_TEST_DPM_LATE, &error))
		error = dpm_suspend_late(PMSG_SUSPEND);
	if (error != 0) {
		pr_err("late suspend of devices failed, error=%d\n", error);
		liblinux_resume_rollback(PLAT_RESUME_FINISH);
		goto exit;
	}

	if (!pm_suspend_should_fail(PM_TEST_DPM_NOIRQ, &error))
		error = dpm_suspend_noirq(PMSG_SUSPEND);
	if (error != 0) {
		pr_err("noirq suspend of devices failed, error=%d\n", error);
		liblinux_resume_rollback(DEV_RESUME_EARLY);
		goto exit;
	}

	if (!pm_suspend_should_fail(PM_TEST_NONBOOT_CPU, &error))
		error = nonboot_cpu_threads_handle(freeze_nonboot_cpu_threads);
	if (error != 0)
		liblinux_resume_rollback(DEV_RESUME_NOIRQ);

	pr_info("liblinux suspend end\n");
exit:
	return error;
#else
	return 0;
#endif
}
EXPORT_SYMBOL(liblinux_suspend);

int liblinux_suspend_late(void)
{
#ifdef CONFIG_LIBLINUX_HAVE_SUSPEND
	int error;

	pr_info("liblinux suspend late start\n");
	error = platform_suspend(PM_SUSPEND_MEM, PLAT_SUSPEND_NOIRQ);
	if (error != 0) {
		pr_err("pltaform failed to prepare noirq, error=%d\n", error);
		liblinux_resume_rollback(PLAT_RESUME_NOIRQ);
		return error;
	}

	arch_suspend_disable_irqs();
	error = syscore_suspend();
	arch_suspend_enable_irqs();
	if (error != 0) {
		pr_err("suspend syscore failed, error=%d\n", error);
		liblinux_resume_rollback(PLAT_RESUME_NOIRQ);
	}
	pr_info("liblinux suspend late end\n");
	return error;
#else
	return 0;
#endif
}
EXPORT_SYMBOL(liblinux_suspend_late);

void liblinux_resume_early(void)
{
#ifdef CONFIG_LIBLINUX_HAVE_SUSPEND
	arch_suspend_disable_irqs();
	syscore_resume();
	arch_suspend_enable_irqs();
	liblinux_resume_rollback(PLAT_RESUME_NOIRQ);
#endif
}
EXPORT_SYMBOL(liblinux_resume_early);
